#include <assert.h>
#include <ctype.h>
#include <errno.h>
#include <limits.h>
#include <signal.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/wait.h>

#include <dirent.h>
#include <fcntl.h>
#include <fnmatch.h>
#include <glob.h>
#include <poll.h>
#include <regex.h>
#include <unistd.h>

#include <err.h>
#include <sysexits.h>

#include "playlist.h"
#include "util.h"

#ifndef nitems
#define nitems(x) (sizeof((x)) / sizeof((x)[0]))
#endif

#ifndef ERR
#define ERR(e, r)		\
	do {			\
		errno = (e);	\
		return (r);	\
	} while (0)
#endif
#define RESERVED_FD(fd)			\
	(((fd) == STDIN_FILENO) ||	\
	((fd) == STDOUT_FILENO) ||	\
	((fd) == STDERR_FILENO))

#define FDELTA 4
#define LINEDELTA 32

#ifndef CMDMAX
#define CMDMAX (PATH_MAX * 16)
#endif
#ifndef INFOMAX
#define INFOMAX 64
#endif
#ifndef INFODEC
#define INFODEC 2
#endif
#ifndef HSIZE
#define HSIZE 32
#endif
#ifndef AUDEV_DEFAULT
#define AUDEV_DEFAULT "exec au2sdl"
#endif

enum {
	PLAY_DONE = 0,
	PLAY_OK,
	PLAY_RETRY
};

struct fentry {
	char	*path;
	int	 fd;
	int	 flags;
};

#define LINE_INITIALIZER { NULL, 0, 0, SIZE_MAX - 1, 0 }
struct line {
	char	*buffer;
	size_t	 count;
	size_t	 size;
	size_t	 max;
	int	 warn;
};

#define CMD_INITIALIZER { NULL, 0, '\0' }
struct cmd {
	char		*arg;
	unsigned long	 count;
	int		 cmd;
};

#define INFO_INITIALIZER { { '\0' }, { '?' }, 0, 0 }
struct info {
	char		cdec[INFODEC + 2];
	char		tdec[INFODEC + 2];
	unsigned long	cint;
	unsigned long	tint;
};

#define PLAY_INITIALIZER {					\
		NULL, LINE_INITIALIZER,	LINE_INITIALIZER,	\
		INFO_INITIALIZER, AUDEV_DEFAULT, NULL, { 0 },	\
		0, 100, STDIN_FILENO, STDOUT_FILENO, 0, 0, 0,	\
		PLRONCE						\
	}
struct play {
	struct playlist	*plist;
	struct line	 iline;
	struct line	 cline;
	struct info	 info;
	char		*cstr;
	char		*dstr;
	struct timeval	 tv;
	size_t		 bsize;
	unsigned long	 ihz;
	int		 in;
	int		 out;
	int		 paused;
	int		 loop;
	int		 retry;
	int		 flags;
};

static pid_t auopen_pid = 0;
static pid_t audev_pid = 0;
static sigset_t sigpend;

static void	 sighandle(int);
static void	 onadd(void *, const char *);
static void	 onrem(void *, const char *);
static int	 playlist_fpadd(struct playlist *, FILE *, int);
static int	 play_single(struct play *, sigset_t *);
static int	 play_exec(struct play *, int *, int *);
static int	 linefd(struct line *, int);
static int	 parsecmd(struct cmd *, char *);
static int	 parseinfo(struct info *, const char *);
static int	 parseseek(struct cmd *, double *);
static void	 printinfo(int, struct info *, const char *, int);
static char	*shquote(const char *);
static int	 ichars(int);
static int	 unreservefd(int);

int
main(int argc, char *argv[])
{
	struct play play = PLAY_INITIALIZER;
	struct fentry *fs = NULL;
	struct fentry *f;
	FILE *fp;
	char *in = NULL;
	char *p;
	sigset_t omask;
	sigset_t smask;
	size_t fcount = 0;
	size_t fsize = 0;
	size_t hsize = HSIZE;
	size_t i;
	unsigned long ul;
	int c;

#ifdef USE_PLEDGE
	(void)pledge("stdio rpath proc exec", NULL);
#endif
	if (setvbuf(stdout, NULL, _IOLBF, 0) != 0)
		err(EX_IOERR, "Could not set line-buffered output");
	play.in = -1;
	play.cline.max = CMDMAX;
	play.iline.max = INFOMAX;
	while ((c = getopt(argc, argv, "F:I:b:c:d:f:g:i:h:lprsv"))
	    != -1)
		switch (c) {
		case 'F':
			errno = 0;
			ul = strtoul(optarg, &p, 0);
			if (errno == 0) {
				if (ul > INT_MAX)
					errno = ERANGE;
				else if (p == optarg || *p != '\0')
					errno = EINVAL;
				else if (ul == STDOUT_FILENO ||
				    ul == STDERR_FILENO)
					errno = EDOM;
			}
			if (errno != 0)
				err(EX_USAGE, "Invalid number '%s'",
				    optarg);
			if (fcntl(ul, F_GETFL, &c) == -1)
				err(EX_NOINPUT,
				    "Invalid file-descriptor %lu", ul);
			f = allot((void **)&fs, &fsize, fcount++,
			    sizeof(struct fentry), FDELTA);
			f->path = NULL;
			f->fd = ul;
			break;
		case 'I':
			errno = 0;
			ul = strtoul(optarg, &p, 0);
			if (errno == 0) {
				if (ul > INT_MAX)
					errno = ERANGE;
				else if (p == optarg || *p != '\0')
					errno = EINVAL;
				else if (ul == STDOUT_FILENO ||
				    ul == STDERR_FILENO)
					errno = EDOM;
			}
			if (errno != 0)
				err(EX_USAGE, "Invalid number '%s'",
				    optarg);
			if (fcntl(ul, F_GETFL, &c) == -1)
				err(EX_NOINPUT,
				    "Invalid file-descriptor %lu", ul);
			in = NULL;
			play.in = ul;
			break;
		case 'b':
			errno = 0;
			ul = strtoul(optarg, &p, 0);
			if (ul > INT_MAX)
				errno = ERANGE;
			if (p == optarg || *p != '\0')
				errno = EINVAL;
			if (errno != 0)
				err(EX_USAGE, "Invalid number %s",
				    optarg);
			play.bsize = ul;
			break;
		case 'c':
			play.cstr = optarg[0] != '\0' ? optarg : NULL;
			break;
		case 'd':
			play.dstr = optarg;
			break;
		case 'f':
			f = allot((void **)&fs, &fsize, fcount++,
			    sizeof(struct fentry), FDELTA);
			f->flags = PLGNONE | PLSCMP;
			if (strcmp(optarg, "-") == 0) {
				f->path = NULL;
				f->fd = STDIN_FILENO;
			} else {
				f->path = optarg;
				f->fd = -1;
			}
			break;
		case 'g':
			f = allot((void **)&fs, &fsize, fcount++,
			    sizeof(struct fentry), FDELTA);
			f->flags = PLGNONE | PLSGLOB;
			if (strcmp(optarg, "-") == 0) {
				f->path = NULL;
				f->fd = STDIN_FILENO;
			} else {
				f->path = optarg;
				f->fd = -1;
			}
			break;
		case 'i':
			if (strcmp(optarg, "-") == 0) {
				in = NULL;
				play.in = STDIN_FILENO;
			} else {
				in = optarg;
				play.in = -1;
			}
			break;
		case 'h':
			errno = 0;
			ul = strtoul(optarg, &p, 0);
			if (ul > SIZE_MAX)
				errno = ERANGE;
			if (p == optarg || *p != '\0')
				errno = EINVAL;
			if (errno != 0 || p == optarg || *p != '\0')
				err(EX_USAGE, "Invalid number %s",
				    optarg);
			hsize = ul;
			break;
		case 'l':
			play.loop = 1;
			break;
		case 'p':
			play.flags = (play.flags & ~PLVTYPE) | PLVFULL;
			break;
		case 'r':
			play.flags = (play.flags & ~PLGTYPE) | PLGRAND;
			break;
		case 's':
			play.flags = (play.flags & ~PLGTYPE) | PLGSHUF;
			break;
		case 'v':
			play.flags = (play.flags & ~PLVTYPE)
			    | PLVSIMPLE;
			break;
		default:
			goto usage;
		}
	argc -= optind;
	argv += optind;
	if (play.in >= 0) {
		for (i = 0; i < fcount; i++)
			if (play.in == fs[i].fd)
				errx(EX_USAGE, "Ambiguous file-"
				    "descriptor %d", fs[i].fd);
	} else if (in != NULL) {
		if ((play.in = open(in, O_RDONLY | O_CLOEXEC)) < 0)
			err(EX_NOINPUT, "Could not open %s", in);
	} else {
		for (i = 0, play.in = STDIN_FILENO; i < fcount; i++)
			if (fs[i].fd == STDIN_FILENO) {
				if ((play.flags & PLVTYPE) == PLVFULL)
					errx(EX_USAGE, "The standard "
					    "input can not be used as "
					    "a file-list");
				if ((play.in = open("/dev/tty",
				    O_RDONLY | O_CLOEXEC)) < 0)
					err(EX_NOINPUT,
					    "Could not open /dev/tty");
				break;
			}
	}
	if ((play.plist = playlist_create(hsize, play.flags)) == NULL)
		err(EX_OSERR, "playlist_create");;
	if ((play.flags & PLVTYPE) == PLVFULL) {
		playlist_onadd(play.plist, &onadd, &play);
		playlist_onrem(play.plist, &onrem, &play);
	}
	if (fs != NULL) {
		for (i = 0; i < fcount; i++) {
			if (fs[i].path != NULL) {
				if ((fp = fopen(fs[i].path, "r"))
				    == NULL)
					warn("Could not open %s",
					    fs[i].path);
			} else if (fs[i].fd >= 0) {
				if ((fp = fdopen(fs[i].fd, "r"))
				    == NULL)
					warn("Could not open file-"
					    "descriptor %d", fs[i].fd);
			} else
				abort();
			if (fp == NULL)
				break;
			(void)playlist_fpadd(play.plist, fp,
			    f[i].flags | (play.flags & ~PLGTYPE &
			    ~PLSTYPE));
			fclose(fp);
		}
		free(fs);
	}
	for (; *argv != NULL; argv++)
		switch ((*argv)[0]) {
		case '(':
			switch (toupper((*argv)[1])) {
			case '\0':
				playlist_gbegin(play.plist, PLGNORM);
				break;
			case 'R':
				playlist_gbegin(play.plist, PLGRAND);
				break;
			case 'S':
				playlist_gbegin(play.plist, PLGSHUF);
				break;
			default:
				warnx("Invalid group type: %s",
				    (*argv) + 1);
				break;
			}
			break;
		case ')':
			if (playlist_gend(play.plist) != 0)
				warnx("Unexpected `)'");
			break;
		default:
			(void)playlist_add(play.plist, *argv,
			    (play.flags & ~PLGTYPE & ~PLSTYPE) |
			    PLSCMP);
		}
	if (play.in < 0 && (play.in = open("/dev/tty", O_RDONLY)) < 0)
		err(EX_NOINPUT, "Could not open /dev/tty");
	sigemptyset(&smask);
	if (sigprocmask(SIG_SETMASK, &smask, &omask) != 0)
		err(EX_OSERR, "sigprocmask");
	smask = omask;
	sigemptyset(&sigpend);
	/*
	 * Ignore SIGTTIN to allow continuous
	 * operation as a background process
	 */
	if (signal(SIGTTIN, SIG_IGN) == SIG_ERR)
		abort();
	/*
	 * SIGCONT is needed if the audio backend disconnects
	 * while the process is stopped.
	 */
	if (signal(SIGCONT, &sighandle) == SIG_ERR)
		abort();
	if (signal(SIGPIPE, SIG_IGN) == SIG_ERR)
		abort();
#ifdef SIGINFO
	if (signal(SIGINFO, &sighandle) == SIG_ERR)
		abort();
#endif
	if ((play.flags & PLVTYPE) != PLVFULL) {
		playlist_onadd(play.plist, &onadd, &play);
		playlist_onrem(play.plist, &onrem, &play);
	}
	while ((c = play_single(&play, &smask)) > 0)
		/* do nothing */;
	playlist_onadd(play.plist, NULL, NULL);
	playlist_onrem(play.plist, NULL, NULL);
	playlist_destroy(play.plist);
	if (play.cline.buffer != NULL)
		free(play.cline.buffer);
	if (play.iline.buffer != NULL)
		free(play.iline.buffer);
	return (c ? EX_SOFTWARE : 0);
usage:
	(void)fprintf(stderr, "usage: auplayer [-lprsv] "
	    "[-F file-descriptor] [-I file-descriptor]\n"
	    "                [-b buffer-size] [-c command] [-d device] "
	    "[-f file-list]\n"
	    "                [-h history-size] [-i input-file] "
	    "file ...\n");
	return (EX_USAGE);
}

void
sighandle(int sig)
{

	(void)sigaddset(&sigpend, sig);
}

void
onadd(void *ctx, const char *path)
{
	struct play *play = ctx;

	assert(ctx != NULL && path != NULL);
	switch (play->flags & PLVTYPE) {
	case PLVSIMPLE:
		(void)dprintf(play->out, "Adding %s\n", path);
		break;
	case PLVFULL:
		(void)dprintf(play->out, "+%s\n", path);
		break;
	}
}

void
onrem(void *ctx, const char *path)
{
	struct play *play = ctx;

	assert(ctx != NULL && path != NULL);
	switch (play->flags & PLVTYPE) {
	case PLVSIMPLE:
		(void)dprintf(play->out, "Removing %s\n", path);
		break;
	case PLVFULL:
		(void)dprintf(play->out, "-%s\n", path);
		break;
	}
}

int
playlist_fpadd(struct playlist *list, FILE *fp, int flags)
{
	char *cp = NULL;
	ssize_t r;
	size_t s = 0;
	size_t i;

	assert(list != NULL && fp != NULL);
	while ((r = getdelim(&cp, &s, '\n', fp)) != -1) {
		for (i = 0; cp[i] != '\0' && isspace(cp[i]); i++)
			/* do nothing */;
		if (i > 0) {
			r -= i;
			(void)memmove(cp, cp + i, r + 1);
		}
		if (r > 0) {
			for (i = r - 1; i > 0 && isspace(cp[i]); i--)
				/* do nothing */;
			r = i + 1;
			cp[r] = '\0';
		}
		if (r == 0)
			continue;
		switch(cp[0]) {
		case '(':
			switch (toupper(cp[1])) {
			case '\0':
				playlist_gbegin(list, PLGNORM);
				break;
			case 'R':
				playlist_gbegin(list, PLGRAND);
				break;
			case 'S':
				playlist_gbegin(list, PLGSHUF);
				break;
			default:
				warnx("Invalid group type: %s", cp + 1);
				break;
			}
			break;
		case ')':
			if (playlist_gend(list) != 0)
				warnx("Unexpected `)'");
			break;
		default:
			(void)playlist_add(list, cp, flags);
			break;
		}
	}
	if (cp != NULL)
		free(cp);
	return (0);
}

int
play_single(struct play *play, sigset_t *smask)
{
	struct pollfd fds[2] = {
		{ -1, POLLIN, 0 }, { -1, POLLIN, 0 }
	};
	struct cmd cmd = CMD_INITIALIZER;
	const char *cur;
	ssize_t r;
	double d;
	int ttyin;
	int cfd = -1;
	int docmd = 0;
	int dead = 0;
	int done = 0;
	int status;

	assert(play != NULL && smask != NULL);
	assert(play->in >= 0 && play->out >= 0);
	if ((cur = playlist_current(play->plist)) == NULL &&
	    (cur = playlist_next(play->plist)) == NULL) {
		if (play->loop) {
			playlist_loop(play->plist);
			if ((cur = playlist_next(play->plist))
			    == NULL && play->cstr != NULL) {
				play->retry = 0;
				return (PLAY_DONE);
			}
		} else if (play->cstr != NULL) {
			play->retry = 0;
			return (PLAY_DONE);
		}
	}
	if ((play->flags & PLVTYPE) == PLVFULL && cur != NULL)
		(void)dprintf(play->out, "@%s\n", cur);
	if (play->cstr != NULL) {
		if (play_exec(play, &cfd, &fds[1].fd) != 0) {
			play->retry = 0;
			return (-1);
		}
	} else {
		if ((cfd = open("/dev/null", O_RDONLY)) == -1) {
			play->retry = 0;
			return (-1);
		}
		fds[1].fd = -1;
	}
	if (play->paused)
		(void)dprintf(cfd, "p\n");
	if ((play->flags & PLVTYPE) == PLVFULL)
		(void)dprintf(cfd, "%luhz\n", play->ihz);
	if (play->retry) {
		(void)dprintf(cfd, "=%lu%s\n", play->info.cint,
		    play->info.cdec);
		(void)warnx("retry: %lu%s\n", play->info.cint,
		    play->info.cdec);
		play->retry = 0;
	}
	if ((play->flags & PLVTYPE) == PLVSIMPLE ||
	    (play->paused && (play->flags & PLVTYPE) == PLVFULL))
		(void)dprintf(cfd, "?\n");
	play->info.cdec[0] = '\0';
	play->info.tdec[0] = '\0';
	play->info.cint = 0;
	play->info.tint = 0;
	ttyin = isatty(play->in);
	fds[0].fd = play->in;
	for (;;) {
		/*
		 * ppoll(2) generates a events for tty input
		 * even when reading would block with SIGTTIN.
		 * This behaviour is quite noisy when using
		 * a tty in raw character mode.
		 */
		if (ppoll(fds, nitems(fds), NULL, smask) < 0) {
			if (errno != EINTR)
				goto err;
			if (sigismember(&sigpend, SIGCONT)) {
				play->retry = 1;
				sigdelset(&sigpend, SIGCONT);
			}
#ifdef SIGINFO
			if (sigismember(&sigpend, SIGINFO)) {
				(void)dprintf(cfd, "?\n");
				sigdelset(&sigpend, SIGINFO);
			}
#endif
			continue;
		}
		if (fds[0].revents &
		    (POLLERR | POLLHUP | POLLNVAL)) {
			(void)kill(auopen_pid, SIGTERM);
			(void)kill(audev_pid, SIGTERM);
			fds[0].fd = -1;
			done = 1;
		}
		if (fds[1].revents &
		    (POLLERR | POLLHUP | POLLNVAL)) {
			if (waitpid(auopen_pid, &status, 0)
			    != auopen_pid)
				goto err;
			dead |= WEXITSTATUS(status);
			if (waitpid(audev_pid, &status, 0) != audev_pid)
				goto err;
			dead |= WEXITSTATUS(status);
			break;
		}
		if (fds[0].revents & POLLIN) {
			while (!(docmd || done || dead) &&
			    (r = linefd(&play->cline, fds[0].fd)) > 0)
				switch (parsecmd(&cmd,
				    play->cline.buffer)) {
				case '\0':
					break;
				case '(':
					switch (toupper(cmd.arg[0])) {
					case '\0':
						playlist_gbegin(
						    play->plist,
						    PLGNORM);
						break;
					case 'R':
						playlist_gbegin(
						    play->plist,
						    PLGRAND);
						break;
					case 'S':
						playlist_gbegin(
						    play->plist,
						    PLGSHUF);
						break;
					default:
						warnx("Invalid group "
						    "type: %s",
						    cmd.arg);
					}
					break;
				case ')':
					if (playlist_gend(play->plist)
					    != 0)
						warnx("Unexpected `)'");
					break;
				case '+':
					/* FALLTHROUGH */
				case '-':
					if (parseseek(&cmd, &d) == 0)
						(void)dprintf(cfd,
						    "%+f\n", d);
					if ((play->flags & PLVTYPE)
					    == PLVSIMPLE)
						(void)dprintf(cfd,
						    "?\n");
					break;
				case '=':
					if (parseseek(&cmd, &d) == 0)
						(void)dprintf(cfd,
						    "=%c%f\n",
						    strchr(cmd.arg, '-')
						    == NULL ? '+' : '-',
						    d < 0 ? -d : d);
					if ((play->flags & PLVTYPE)
					    == PLVSIMPLE)
						(void)dprintf(cfd,
						    "?\n");
					break;
				case '/':
					cmd.cmd = '\0';
					docmd = playlist_search(
					    play->plist, cmd.arg,
					    PLSAUTO | PLGOTO |
					    (play->loop ? PLLOOP : 0))
					    != NULL;
					break;
				case '?':
					(void)dprintf(cfd, "?\n");
					break;
				case 'L':
					play->loop = !play->loop;
					(void)dprintf(play->out,
					    "%sloop\n", play->loop ?
					    "" : "no-");
					break;
				case 'V':
					switch (play->flags & PLVTYPE) {
					case PLVQUIET:
						play->flags =
						    (play->flags &
						    ~PLVTYPE) |
						    PLVSIMPLE;
						(void)dprintf(play->out,
						    "verbose\n");
						break;
					case PLVSIMPLE:
						play->flags =
						    (play->flags &
						    ~PLVTYPE) |
						    PLVQUIET;
						(void)dprintf(play->out,
						    "quiet\n");
						break;
					}
					break;
				case 'a':
					if (fds[1].fd >= 0)
						(void)playlist_add(
						    play->plist,
						    cmd.arg,
						    (play->flags &
						    ~PLGTYPE &
						    ~PLSTYPE) |
						    PLSGLOB);
					else
						playlist_append(
						    play->plist,
						    cmd.arg);
					break;
				case 'd':
					if (cmd.arg[0] == '\0') {
						dead = 1;
						play->retry = 0;
						break;
					}
					playlist_rem(play->plist,
					    cmd.arg, PLSGLOB);
					if (playlist_current(
					    play->plist) == NULL) {
						cmd.cmd = '\0';
						docmd = 1;
					}
					break;
				case 'h':
					if (cmd.count > 0)
						docmd = 1;
					break;
				case 'i':
					(void)dprintf(cfd, "?\n");
					break;
				case 'j':
					/* FALLTHROUGH */
				case 'k':
					/* FALLTHROUGH */
				case 'l':
					if (cmd.count > 0)
						docmd = 1;
					break;
				case 'p':
					(void)dprintf(cfd, "p\n");
					switch (play->flags & PLVTYPE) {
					case PLVSIMPLE:
						(void)dprintf(play->out,
						    "%spaused\n",
						    play->paused ?
						    "un" : "");
						break;
					case PLVFULL:
						(void)dprintf(play->out,
						    "!%spaused\n",
						    play->paused ?
						    "un" : "");
						break;
					}
					play->paused = !play->paused;
					break;
				case 'q':
					done = 1;
					break;
				case 'r':
					(void)dprintf(cfd, "=0\n");
					break;
				case 'z':
					play->ihz = cmd.count;
					if ((play->flags & PLVTYPE)
					    == PLVFULL)
						(void)dprintf(play->out,
						    "!hz %lu\n",
						    play->ihz);
					(void)dprintf(cfd, "%luhz\n",
					    play->ihz);
					break;
				default:
					warnx("Invalid command: '%s'",
					    play->cline.buffer);
					break;
				}
			/*
			 * When SIGTTIN is ignored read(2), on
			 * a tty, returns with EIO, to allow use
			 * as a background process this is
			 * case is ignored.
			 */
			if (r < 0 && errno != EAGAIN &&
			    (!ttyin || errno != EIO)) {
				if (errno == 0)
					done = 1;
				else
					warn("command input");
			}
			if (docmd || done || dead) {
				if (fds[1].fd == -1)
					break;
				(void)kill(auopen_pid, SIGTERM);
				(void)kill(audev_pid, SIGTERM);
				fds[0].fd = -1;
			}
		}
		if (fds[1].revents & POLLIN) {
			while ((r = linefd(&play->iline, fds[1].fd))
			    > 0)
				if (parseinfo(&play->info,
				    play->iline.buffer) == 0)
					printinfo(play->out,
					    &play->info, cur,
					    (play->flags & PLVTYPE)
					    == PLVFULL);
				else
					warn("parseinfo: %s",
					    play->iline.buffer);
			if (r < 0 && errno != 0 && errno != EAGAIN)
				warn("linefd");
		}
	}
	(void)close(cfd);
	if (fds[1].fd >= 0)
		(void)close(fds[1].fd);
	if (dead) {
		if (play->retry)
			return (PLAY_RETRY);
		if (cur != NULL)
			playlist_rem(play->plist, cur, PLSCMP);
		cmd.cmd = '\0';
		docmd = 1;
	}
	if (done)
		return (PLAY_DONE);
	if (!docmd) {
		cmd.cmd = 'l';
		cmd.count = 1;
	}
	switch (cmd.cmd) {
	case '\0':
		break;
	case 'h':
		/* FALLTHROUGH */
	case 'j':
		for (; cmd.count > 0; cmd.count--)
			playlist_prev(play->plist);
		break;
	case 'k':
		/* FALLTHROUGH */
	case 'l':
		for (; cmd.count > 0; cmd.count--)
			if (playlist_next(play->plist) == NULL) {
				if (!play->loop)
					return (PLAY_DONE);
				playlist_loop(play->plist);
				if (playlist_next(play->plist) == NULL)
					return (PLAY_DONE);
			}
		break;
	default:
		abort(); /* NOTREACHED */
	}
	return (PLAY_OK);
err:
	close(cfd);
	close(fds[1].fd);
	return (-1);
}

int
play_exec(struct play *play, int *cfd, int *ifd)
{
	/*
	 * auopen <file> -C <cfd> -I <ifd> [-b <bsize>] [-v] |
	 *     <cmd> -R [-b <bsize> [-d <dev>]
	 */
	const char *cur;
	char *argv[10] = { NULL };
	char *cmd;
	char *dev;
	int cpipe[2];
	int fpipe[2];
	int ipipe[2];
	char *cp;
	size_t i;

	assert(play != NULL && cfd != NULL && ifd != NULL);
	if ((cur = playlist_current(play->plist)) == NULL)
		return (-1);
	if (pipe(cpipe) != 0)
		err(EX_OSERR, "pipe(2)");
	if (pipe(fpipe) != 0)
		err(EX_OSERR, "pipe(2)");
	if (pipe(ipipe) != 0)
		err(EX_OSERR, "pipe(2)");
	cpipe[0] = unreservefd(cpipe[0]);
	ipipe[1] = unreservefd(ipipe[1]);
	if ((auopen_pid = fork()) < 0) {
		err(EX_OSERR, "Could not fork auopen(1)");
	} else if (auopen_pid == 0) {
		close(cpipe[1]);
		close(fpipe[0]);
		close(ipipe[0]);
		if (fpipe[1] != STDOUT_FILENO) {
			dup2(fpipe[1], STDOUT_FILENO);
			close(fpipe[1]);
		}
		if ((cp = malloc(ichars(cpipe[0]) + 1 +
		    ichars(ipipe[1]) + 1 + (play->bsize > 0 ?
		    ichars(play->bsize) + 1 : 0))) == NULL)
			err(EX_OSERR, "Could not allocate argv");
		i = 0;
		argv[i++] = "auopen";
		argv[i++] = (char *)cur;
		argv[i++] = "-C";
		argv[i++] = cp;
		cp += sprintf(cp, "%d", cpipe[0]) + 1;
		argv[i++] = "-I";
		argv[i++] = cp;
		cp += sprintf(cp, "%d", ipipe[1]) + 1;
		if ((play->flags & PLVTYPE) == PLVFULL)
			argv[i++] = "-v";
		if (play->bsize > 0) {
			argv[i++] = "-b";
			argv[i++] = cp;
			cp += sprintf(cp, "%zu", play->bsize);
		}
		execvp("auopen", argv);
		err(EX_OSERR, "Could not execute '%s %s'", "auopen",
		    cur != NULL ? cur : "(null)");
	}
	close(cpipe[0]);
	close(fpipe[1]);
	close(ipipe[1]);
	cmd = play->cstr != NULL ? play->cstr : AUDEV_DEFAULT;
	if ((audev_pid = fork()) < 0) {
		err(EX_OSERR, "Could not fork '%s'", cmd);
	} else if (audev_pid == 0) {
		close(cpipe[1]);
		close(ipipe[0]);
		if (fpipe[0] != STDIN_FILENO) {
			dup2(fpipe[0], STDIN_FILENO);
			close(fpipe[0]);
		}
		dev = play->dstr != NULL ? shquote(play->dstr) : NULL;
		if ((cp = malloc(strlen(cmd) + (dev == NULL ? 0 :
		    3 + strlen(dev) + 1) + (play->bsize > 0 ?
		    3 + ichars(play->bsize) + 1 : 0))) == NULL)
			err(EX_OSERR,
			    "Could not allocate command string");
		i = 0;
		i += sprintf(cp + i, "%s", cmd);
		if (dev != NULL) {
			i += sprintf(cp + i, " -d%s", dev);
			free(dev);
		}
		if (play->bsize > 0)
			i += sprintf(cp + i, " -b%zu", play->bsize);
		execl("/bin/sh", "sh", "-c", cp, NULL);
		err(EX_OSERR, "Could not execute '%s'", argv[0]);
	}
	close(fpipe[0]);
	*cfd = cpipe[1];
	*ifd = ipipe[0];
	return (0);
}

int
linefd(struct line *line, int fd)
{
	ssize_t r;
	int n;

	if (ioctl(fd, FIONREAD, &n) == -1)
		return (-1);
	errno = 0;
	for (; n > 0 && (r = read(fd, line->count >= line->max ?
	    line->buffer : allot((void **)&line->buffer, &line->size,
	    line->count, 1, LINEDELTA), 1)) > 0; n--) {
		if (line->count >= line->max && !line->warn) {
			line->warn = 1;
			warnx("Line too long");
		}
		if (line->count >= line->max) {
			if (*line->buffer == '\n') {
				line->warn = 0;
				line->count = 0;
			}
		} else if (line->buffer[line->count] == '\n')
			goto out;
		else
			line->count++;
	}
	if (errno == 0)
		errno = EAGAIN;
	return (-1);
out:
	*(char *)allot((void **)&line->buffer, &line->size, line->count,
	    1, LINEDELTA) = '\0';
	r = line->count;
	line->count = 0;
	return (r);
}

int
parsecmd(struct cmd *cmd, char *s)
{
	char *p;

	assert(cmd != NULL && s != NULL);
	while (isspace(*s))
		s++;
	if (*s >= '0' && *s <= '9') {
		errno = 0;
		cmd->count = strtoul(s, &p, 10);
		if (errno != 0) {
			warn("parsecmd");
			return (-1);
		}
		s += p - s;
	} else
		cmd->count = 1;
	while (isspace(*s))
		s++;
	cmd->cmd = *s;
	if (*s != '\0')
		while (isspace(*++s))
			/* do nothing */;
	cmd->arg = s;
	return (cmd->cmd);
}

int
parseinfo(struct info *info, const char *s)
{
	char *p;
	size_t i;

	assert(info != NULL && s != NULL);
	while (isspace(*s))
		s++;
	if (*s == '>') {
		errno = ERANGE;
		return (-1);
	}
	errno = 0;
	info->cint = strtoul(s, &p, 10);
	if (errno != 0)
		return (-1);
	s += p - s;
	info->cdec[0] = '\0';
	if (*s == '.') {
		for (i = 1, s++; *s >= '0' && *s <= '9'; s++)
			if (i < sizeof(info->cdec) - 1)
				info->cdec[i++] = *s;
		if (i > 1)
			info->cdec[0] = '.';
		do
			info->cdec[i] = '\0';
		while (info->cdec[--i] == '0');
		if (info->cdec[i] == '.')
			info->cdec[i] = '\0';
	}
	if (*s != '/') {
		errno = EINVAL;
		return (-1);
	}
	s++;
	errno = 0;
	info->tint = strtoul(s, &p, 10);
	if (errno == 0) {
		s += p - s;
		info->tdec[0] = '\0';
		if (*s == '.') {
			for (i = 1, s++; *s >= '0' && *s <= '9'; s++)
				if (i < sizeof(info->tdec) - 1)
					info->tdec[i++] = *s;
			if (i > 1)
				info->tdec[0] = '.';
			do
				info->tdec[i] = '\0';
			while (info->tdec[--i] == '0');
			if (info->tdec[i] == '.')
				info->tdec[i] = '\0';
		}
	} else
		(void)strcpy(info->tdec, "?");
	return (0);
}

int
parseseek(struct cmd *cmd, double *out)
{
	double d = 0;
	const char *s;
	char *p;

	assert(cmd != NULL && out != NULL);
	if ((s = cmd->arg) == NULL || *s == '\0')
		return (-1);
	do {
		errno = 0;
		d += strtod(s, &p);
		if (errno != 0 || s == p || (*p != ':' && *p != '\0')) {
			warnx("Invalid number '%s'", cmd->arg);
			return (-1);
		}
		for (s += p - s; *s == ':'; s++)
			d *= 60;
	} while (*s != '\0');
	*out = (cmd->cmd == '-' ? -d : d) * cmd->count;
	return (0);
}

void
printinfo(int fd, struct info *info, const char *file, int raw)
{

	if (raw) {
		if (info->tdec[0] != '?')
			(void)dprintf(fd, ":%lu%s/%lu%s\n", info->cint,
			    info->cdec, info->tint, info->tdec);
		else
			(void)dprintf(fd, ":%lu%s\n", info->cint,
			    info->cdec);
		return;
	}
	if (file != NULL)
		(void)dprintf(fd, "%s: ", file);
	if (info->tint >= 3600) {
		(void)dprintf(fd,
		    "%lu:%.2lu:%.2lu%s/%lu:%.2lu:%.2lu%s\n",
		    info->cint / 3600, info->cint / 60 % 60,
		    info->cint % 60, info->cdec, info->tint / 3600,
		    info->tint / 60 % 60, info->tint % 60, info->tdec);
	} else if (info->tint >= 60) {
		(void)dprintf(fd, "%lu:%.2lu%s/%lu:%.2lu%s\n",
		    info->cint / 60, info->cint % 60, info->cdec,
		    info->tint / 60, info->tint % 60, info->tdec);
	} else if (info->tdec[0] != '?') {
		(void)dprintf(fd, "%lu%s/%lu%s\n", info->cint,
		    info->cdec, info->tint, info->tdec);
	} else if (info->cint >= 3600) {
		(void)dprintf(fd, "%lu:%.2lu:%.2lu%s\n",
		    info->cint / 3600, info->cint / 60 % 60,
		    info->cint % 60, info->cdec);
	} else if (info->cint >= 60) {
		(void)dprintf(fd, "%lu:%.2lu%s\n", info->cint / 60,
		    info->cint % 60, info->cdec);
	} else
		(void)dprintf(fd, "%lu%s\n", info->cint, info->cdec);
}

char *
shquote(const char *s)
{
	char *r;
	size_t i;
	size_t n;

	assert(s != NULL);
	for (i = 0, n = 2; s[i] != '\0'; i++, n++)
		if (s[i] == '\'')
			n += 3;
	if ((r = malloc(n + 1)) == NULL)
		err(EX_OSERR, "Could not allocate quoted string");
	r[0] = '\'';
	for (i = 0, n = 1; s[i] != '\0'; i++, n++) {
		if (s[i] == '\'') {
			r[n++] = '\'';
			r[n++] = '\\';
			r[n++] = '\'';
		}
		r[n] = s[i];
	}
	r[n] = '\0';
	return (r);
}

int
ichars(int v)
{
	int r = 1;

	if (v < 0) {
		r++;
		v = -v;
	}
	for (r = 1; v >= 10; v /= 10, r++)
		/* do nothing */;
	return (r);
}

int
unreservefd(int fd)
{
	int nfd;
	int fl;

	if (fd < 0 || !RESERVED_FD(fd))
		return (fd);
	for (nfd = 0; RESERVED_FD(nfd) ||
	    fcntl(nfd, F_GETFL, &fl) != -1; nfd++)
		if (nfd == INT_MAX)
			errc(EX_SOFTWARE, ENFILE,
			    "Could not find a file descriptor");
	if (dup2(fd, nfd) == -1)
		err(EX_OSERR, "Could not duplicate a file descriptor");
	close(fd);
	return (nfd);
}
